package networkTraining.algorithms;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import neuralModel.neuron.INeuron;
import neuralModel.neuron.ISignal;
import neuralNetwork.INeuralNetwork;

public class BackPropagationAlgorithm implements ITrainingAlgorithm
{
	private INeuralNetwork neuralNetwork;

	private Map<INeuron, Double> errorDeltaMap;
	private Map<ISignal, Double> weightDeltaMap;

	private double learningRate;
	private double momentum;

	public BackPropagationAlgorithm(INeuralNetwork neuralNetwork, double learingRate, double momentum)
	{
		this.neuralNetwork = neuralNetwork;
		this.learningRate = learingRate;
		this.momentum = momentum;

		errorDeltaMap = new HashMap<INeuron, Double>();
		weightDeltaMap = new HashMap<ISignal, Double>();

		for (ISignal signal : neuralNetwork.getSignalList())
			weightDeltaMap.put(signal, 0.0);
	}

	@Override
	public void trainNeuralNetwork(Map<String, Double> errorMap)
	{
		calculateDeltas(errorMap);
		updateWeights();
	}

	public void calculateDeltas(Map<String, Double> errorMap)
	{
		double error, deltaValue;
		INeuron neuron;

		// find error delta for output layer
		for (String neuronName : errorMap.keySet())
		{
			error = errorMap.get(neuronName);
			neuron = neuralNetwork.getNeuron(neuronName);
			deltaValue = error * neuron.getActivationFunction().calculateDerivative(neuron.getInducedLocalField());
			errorDeltaMap.put(neuron, deltaValue);
		}

		// iterate over hidden layers in reverse order
		List<INeuron> networkLayer;
		for (int i = neuralNetwork.getLayerCount() - 2; i > 0; i--)
		{
			networkLayer = neuralNetwork.getLayer(i);
			for (INeuron currentNeuron : networkLayer)
			{
				deltaValue = 0;
				for (ISignal signal : currentNeuron.getOutputSignalList())
				{
					deltaValue += (signal.getWeight() * errorDeltaMap.get(signal.getTargetNeuron()));
				}
				deltaValue *= currentNeuron.getActivationFunction().calculateDerivative(currentNeuron.getInducedLocalField());
				errorDeltaMap.put(currentNeuron, deltaValue);
			}
		}
	}

	public void updateWeights()
	{
		double errorDelta;
		double newWeightDelta;
		double oldWeightDelta;
		for (ISignal signal : neuralNetwork.getSignalList())
		{
			errorDelta = errorDeltaMap.get(signal.getTargetNeuron());
			oldWeightDelta = weightDeltaMap.get(signal);
			newWeightDelta = (momentum * oldWeightDelta) + (learningRate * errorDelta * signal.getSignalValue());
			weightDeltaMap.put(signal, newWeightDelta);

			signal.setWeight(signal.getWeight() + newWeightDelta);
		}
	}
}
